<?php
require_lib('net/Snoopy.class',false);

/* other taobao url
define('TAIBAO_API_AUTHCODEHOST','http://open.taobao.com/isv/authorize.php?appkey=');
define('TAIBAO_API_SESSIONHOST','http://container.api.taobao.com/container?authcode=');
define('TAIBAO_API_SESSIONHOST_SANDBOX','http://container.api.tbsandbox.com/container?authcode=');
*/

/** 
 * 传送淘宝接口[简称taobao]API参数到网店,得到相应返回值。
 * @author alex  
 * @version v1.0  2010-5-30
 */
class TaoBaoRequest {
	/**
	 * @var string $host_url taobao API的网站地址
	 */	
	var $_host_url='http://gw.api.taobao.com/router/rest';
	/**
	 * @var string $_sandbox_url taobao 沙箱测试环境的网站地址
	 */		
	var $_sandbox_url='http://gw.api.tbsandbox.com/router/rest';
	
	/**
	 * @var array $app_token 应用app访问taobao的令牌，参数包括：<br/>
	 * string app_key : 必须，  API应用的标志符.<br/>
	 * string app_secret :必须， 用户登录appSecret，用于用户鉴权，将相关参数通过md5,hmac加密为一个sign值<br/>
	 * string app_session   :可选，淘宝会话，需要会话的接口必须提供 
	 */
	var $app_token=array();
	
	/**
	 * @var array $app_setting 应用app访问taobao的通用参数设置，一般不改变默认值，包括：<br/>
	 * string format：响应格式。xml,json，默认是 'json'。<br/>
	 * strng  sign_method :签名方法，md5,hmac，默认是 'md5'。<br/>
	 * string v：API接口版本号，2.0<br/>
	 * strng  timestamp:时间戳，格式为yyyy-mm-dd hh:mm:ss，例如：2008-01-25 20:23:30。淘宝API服务端允许客户端请求时间误差为10分钟<br/>
	 */
	var $app_setting=array('format'=>'json',     //json , xml
	                       'sign_method'=>'hmac', // md5 , hmac
	                       'v'=>'2.0', 'timestamp'=>'2010-06-01 12:00:00');
	 
	/**
	 *  @var string $url 上次调用API接口的URL.
	 */
	var $url;
	
	/**
	 * @var bool $isRunInSandBox 是否在沙箱测试环境下运行，默认true
	 */
	var $isRunInSandBox=true;
	
	/**
	 * @var int HTTPMETHOD_POST $httpmethod使用POST方法传递API参数，$httpmethod默认使用HTTPMETHOD_POST
	 */ 
	const  HTTPMETHOD_POST=0; 
	/**
	 * @var int HTTPMETHOD_POST $httpmethod使用GET方法传递API参数
	 */ 	
	const  HTTPMETHOD_GET=1;
	/**
	 * @var int HTTPMETHOD_POSTWITHFILE $httpmethod使用POST方法传递API参数，并且还要上传文件<br/>
	 * 如上传图像，array('image' => '/tmp/cs.jpg')
	 */ 		
	const  HTTPMETHOD_POSTWITHFILE=2;
	/**
	 * 
	 * @var int $httpmethod 传递API参数的方法，其值是：@see HTTPMETHOD_POST,@see HTTPMETHOD_GET,@see HTTPMETHOD_POSTWITHFILE <br/>
	 * 默认为HTTPMETHOD_POST，如果参数不合法，使用HTTPMETHOD_POST。
	 */
	var $httpmethod=0;
	/**
	 * @var array $postWithFiles 当$httpmethod=HTTPMETHOD_POSTWITHFILE时，使用此参数设置的文件上传<br/>
	 * 如上传图像，array('image' => '/tmp/cs.jpg')
	 */
	var $postWithFiles=array();
	
	
    function __construct(){
    	$time_zone=@date_default_timezone_get();
    	if(! $time_zone || $time_zone=='UTC')
    	    date_default_timezone_set('Asia/Shanghai');
    	$this->httpmethod=self::HTTPMETHOD_POST;
    }

	/**
	 * 得到taobo API的返回值，JSON,XML字符串。
	 * @param string $cmdid 要调用的api接口的cmdid字符串，如user.getUserInfo，如果为空，将返回false.
	 * @param array $paramArr 要调用的api接口的参数数组，一般通用的参数不要传，如charset，format等。
	 * @return string|bool 调用的api接口得到的JSON,XML字符串。
	 */
	function &get($cmdid,array $paramArr){
		if(! $cmdid) return false; //count($paramArr)==0,some function is empty
		if(! $this->app_token['app_key'] || ! $this->app_token['app_secret']) 
		   return false;
		$this->app_setting['timestamp']=date('Y-m-d H:i:s');
		$host_url=$this->isRunInSandBox ? $this->_sandbox_url : $this->_host_url;
		
		//组装url,param array.
		$paramArr=$paramArr + $this->app_setting;
		$paramArr['app_key']=$this->app_token['app_key'];
		if($this->app_token['app_session'])
		    $paramArr['session']=$this->app_token['app_session'];
		$paramArr['method']=$cmdid;
		$paramArr['sign']=$this->_createSign($paramArr);
		
		$this->url = $host_url;
		
		//call taobao method
        $snoopy = new Snoopy();
        if($this->app_setting['format']=='xml')
          $snoopy->accept='text/xml';
        else
          $snoopy->accept='text/json';
        if($this->httpmethod == self::HTTPMETHOD_POST)
          $snoopy->submit($this->url,$this->_prepareParam($paramArr)); 
        elseif ($this->httpmethod == self::HTTPMETHOD_GET){
          $this->url .= '?' . $this->_createStrParam($paramArr);      
          $snoopy->fetch($this->url);
        }
        elseif ($this->httpmethod== self::HTTPMETHOD_POSTWITHFILE 
               && count($this->postWithFiles)>0){
          $snoopy->_submit_type='multipart/form-data';
          $snoopy->submit($this->url,$this->_prepareParam($paramArr),$this->postWithFiles);
        }
        else 
          $snoopy->submit($this->url,$this->_prepareParam($paramArr)); 
        
        if($snoopy->error)
          throw new Exception($snoopy->error);
        return $snoopy->results;
	}
	
	/**
	 * 得到 API的返回值，JSON,XML生成的数组。
	 * @param string $cmdid 要调用的api接口的cmdid字符串，如果为空，将返回false.
	 * @param array $paramArr 要调用的api接口的参数数组，一般通用的参数不要传，如charset，format等。
	 * @param string $handleGetResultFunc 用于额外处理get函数返回字符串的函数名称，此函数调用在调用get函数后，解析为数组前，默认为NULL不调用。
	 * @return array|bool 调用的api接口得到的 JSON,XML生成的数组。
	 */	
	function &getArray($cmdid,array $paramArr,$handleGetResultFunc=NULL){
		//parse xml format
		if($this->app_setting['format']=='xml'){  
			$result=&$this->get($cmdid,$paramArr);
			
			if($handleGetResultFunc)  //addition handle result 
				try{$result = $handleGetResultFunc($result);}catch (Exception  $e){}
				
			$arr=array(); 
			if(!$this->parseXmlString($result,$arr)) return false;
			return $arr;
		}
		
		//parse json format
		if(!($result=&$this->get($cmdid,$paramArr))) return false;
		
		if($handleGetResultFunc)  //addition handle result 
			try{$result = $handleGetResultFunc($result);}catch (Exception  $e){}
		$result = preg_replace('/(":)(\d{9,})/i', '${1}"${2}"', $result); //taobao bug,number >2^32
		
		$arr=json_decode($result,true);
		if ($arr == NULL) { //normal kick invalid json
			$patterns = array ('/,+\s*\}/', '/,+\s*\]/', '/"\s+|\s+"/','/\n|\r/' );
			$replacements = array ('}', ']', '"',' ' );
			$result = preg_replace ( $patterns, $replacements, $result );
			$arr = json_decode ( $result, true );
		}
		return $arr;
	}	
	
	/**
	 * 检查接口调用get*函数返回是否有错误
	 * @param  mixed $result  接口调用get*函数返回值.
	 * @return bool true 接口调用有效，false 接口调用出错.
	 */
	function checkResult(&$result,&$errorCode,&$errorMsg){
		$errorCode=0;
		$errorMsg=''; 		
		if(is_array($result)){
			if(! array_key_exists('error_response',$result))
			    return true;
			else{
				$error=$result['error_response'];
				$errorCode=$error['code'];
				$errorMsg=$error['msg'];
				if(array_key_exists('sub_code',$error))
				   $errorMsg .= ";\n".$error['sub_code'];
				if(array_key_exists('sub_msg',$error))
				   $errorMsg .= ";\n".$error['sub_msg'];
				
			}
		}elseif (is_object($result)){
			if(! property_exists($result,'error_response'))
			   return true;
			else{
				$error=$result->error_response;
				$errorCode=$error->code;
				$errorMsg=$error->msg;
				if(property_exists($error,'sub_code'))
				   $errorMsg .= ";\n".$error->sub_code;
				if(property_exists($error,'sub_msg'))
				$errorMsg .= ";\n".$error->sub_msg;
				
			}
		}
		elseif (is_string($result)){
			$match=array();
			if(! preg_match('/^\{"error_response":\{.+,"code":(\d*),"msg":"(.+)\}\}$/',$result,$match))
			     return true;
			else{
				$errorCode=$match[1];
				$submatch=array();
				if(preg_match('/(.+)","sub_code":"(.*)","sub_msg":"(.*)"/',$match[2],$submatch)) //sub_code and sun_msg all
					$errorMsg = $submatch[1].";\n".$submatch[2].";\n".$submatch[3];
				else $errorMsg=rtrim($match[2],'"');
				
			}
		}
		elseif(is_bool($result))
		 return $result;

		return false;
	}

	/*
	 * 组参函数 ,内部用
	 */
	private function _createStrParam(array &$paramArr) {
		$strParam = '';
		foreach ( $paramArr as $key => $val ) {
			if ($key != '' && isset($val)) {
				if(is_array($val)) {
					if( count($val)>0)
					foreach ($val as $item)
					  $strParam .= $key . '=' . urlencode( $item ) . '&';
				}
				else 
				$strParam .= $key . '=' . urlencode( $val ) . '&';
			}
		}
		return $strParam;
	}	
	private function _prepareParam(array &$paramArr) { //trim empty value
		$Paramok =array();
		foreach ( $paramArr as $key => $val ) {
			if ($key != '' && isset($val)) {
				if(is_array($val)) {
					$itemarr=array();
					foreach ($val as $item)
					  if($item != '')  $itemarr[]=$item;
					$Paramok[$key] = $itemarr ;
				}
				else 
				$Paramok[$key] = $val; //Snoopy类在执行submit函数时，它自动会将参数做urlencode编码
			}
		}
		return $Paramok;
	}	
	/*
	 * 签名函数,内部用 
	 */
    private function _createSign(array $paramArr) {
		$sign = '';
		ksort ( $paramArr );
		foreach ( $paramArr as $key => $val ) {
			if ($key != '' && isset($val)) {
				if (is_array ( $val )) {
					if(count ( $val ) > 0)
					foreach ( $val as $item )
						$sign .= $key . $item;
				} 
				else
					$sign .= $key . $val;
			}
		}	
		$app_secret=$this->app_token['app_secret'];
		if($this->app_setting['sign_method']==='hmac')
		 $sign = strtoupper(hash_hmac('md5',$sign,$app_secret));
		else
		 $sign = strtoupper(md5($app_secret.$sign . $app_secret));
		return $sign;
	}
	/**
	 * xml字符串解析为数组，xml字符串如<orders><order orderid="97913707" merchantid="6619" 
	 * paysum="32.90" isdeleted="0" ispaid="0" status="1" merchantorderid="" /></orders>
	 * <br/>如果不是xml字符串，不解析。<br/>原如果解析失败，抛出Execption异常
	 * @param string $xmlString xml字符串
	 * @param array $resultarr 解析后的结果数组
	 */
	function parseXmlString(&$xmlString,array &$arr){
		$xmlString=trim($xmlString);
		if(strpos($xmlString,'<')!==0) return false; //simple test is xml
		try{
		  $root =new SimpleXMLElement($xmlString);
		  $resultarr=array();
		  $this->__fillArray($root,$resultarr);
		  if(strpos($xmlString,'<error_response>')===false)
		     $arr['response']=$resultarr; //set taobao return val form
		  else 
		     $arr['error_response']=$resultarr; //set taobao error
		  return true;
		}catch (Exception $e){
			throw new Exception('返回xml解析错误:' . $e->getMessage());
		}
	}	
	//----private function ------//
	function __fillArray($root,array &$resultarr){
		if(! $root) return;
		$rootarr=get_object_vars($root);
		$isList=false;
		foreach ($rootarr as $key=>$val) {
			if($key=='@attributes'){
				$isList=$val['list'];   //is array
			} 
			else if(is_object($val)){
				$newarr=array();
				$this->__fillArray($val,$newarr);
				if($isList)
				  $resultarr[$key]=array($newarr);   //one element array
				else
				   $resultarr[$key]=$newarr;
			}
			else if(is_array($val) && count($val)>0){//is array
				$valarr=array();
				for($i=0;$i<count($val);$i++){
					$newarr=array();
					$this->__fillArray($val[$i],$newarr);
					$valarr[]=$newarr;
				}
				$resultarr[$key]=$valarr;
			}
			else
			 $resultarr[$key]=$val;
		}		
	}
	
}

